// URL support
// Copyright 2004 (C) Ralph Thomas

#include <model/internal/url.h>
#include <util/regexp.h>
#include <map>

//
// This regexp was manually derived from the BNF in RFC1738.
//
// We allow spaces in our paths, though. Eventually this should be replaced
// with "proper" URL code.
//
#define URL_REGEXP "([A-Za-z]+)://([A-Za-z0-9\\.\\-:]+@)?([A-Za-z0-9\\.\\-]*)(:[0-9]+)?/([A-Za-z0-9$\\-_\\.\\+!\\*'\\(\\),%/ ]*)(\\?.*)?"

namespace model {
	class urlImpl : public url, private util::regexp::stringProcessor {
	  private:
		typedef std::map<util::string, util::string> argMap;
		util::string theUrl;
		util::string protocol;	
		util::string username;
		util::string password;
		util::string hostname;
		int port;
		util::string path;
		util::string query;
		int presentParts;
		argMap args;
		//
		// Interpret the results of the regexp search.
		//
		virtual void processString( const util::regexp::matchedString* str ) {
			presentParts = 0;
			theUrl = str->getMatchedItem( 0 );
			protocol = str->getMatchedItem( 1 );

			presentParts |= kProtocol;

			if( str->getMatchedItem( 2 ) != "" ) {
				util::string userInfo = str->getMatchedItem( 2);

				//
				// We have a username at least, and maybe a
				// password too.
				//
				presentParts |= kUsername;

				//
				// We need to remove the '@' from the end and
				// check to see if there's a ':' in the middle
				// (delimiting username and password).
				//
				userInfo.resize( userInfo.size() - 1 );
				//
				// See if there's a ':' in the middle.
				// XXX: This will fail if there's a username
				//	which has a ':' in the middle.
				//
				int colon = (int)userInfo.find( ":" );
				if( colon == -1 ) {
					//
					// There was no colon, that probably
					// means that there's just a username
					// here, 
					username = userInfo;
				} else {
					//
					// There's a colon, so there's a
					// password present.
					//
					presentParts |= kPassword;
					username = userInfo.substr( 0, colon );
					password = userInfo.substr( colon+1, userInfo.size() );
				}
			}

			if( str->getMatchedItem( 3 ) != "" ) {
				hostname = str->getMatchedItem( 3 );
				presentParts |= kHostname;
			}

			if( str->getMatchedItem( 4 ) != "" ) {
				util::string ports = str->getMatchedItem( 4 );
				//
				// Remove the colon at the start.
				//
				if( ports[0] == ':' ) ports.erase( ports.begin() );
				port = strtol( ports.c_str(), NULL, 10 );
				presentParts |= kPort;
			}

			if( str->getMatchedItem( 5 ) != "" ) {
				path = str->getMatchedItem( 5 );
				presentParts |= kPath;
			}

			if( str->getMatchedItem( 6 ) != "" ) {
				//
				// Now we get to iterate through the query
				// string, extracting all of the key/value
				// pairs.
				//
				presentParts |= kQuery;
				query = str->getMatchedItem( 6 );
				util::string::iterator i = query.begin();
				//
				// Remove the question mark from the beginning
				// of the query string.
				//
				if( *i == '?' ) {
					query.erase( i ); i = query.begin();
				}
				//
				// This little parser has two states. The
				// initial state is kKey.
				//
				enum states {
					kKey,	///< Currently parsing a key
					kValue,	///< Currently parsing a value
				};
				states cState = kKey;
				util::string value, key;
				//
				// Iterate through the string, removing it's
				// characters as we go.
				//
				while( i != query.end() ) {
					if( *i == '=' ) {
						cState = kValue;
					} else if( *i == '&' ) {
						//
						// The & character is used to
						// delimit pairs. When we encounter
						// it we push the current pair
						// into our map, and start looking
						// for keys again.
						//
						cState = kKey;
						args[key] = value;
						key.clear(); value.clear();
					} else {
						//
						// Regular character; add it to
						// the current thing (either a key
						// or value, depending on state).
						//
						if( cState == kKey ) key += *i;
						else value += *i;
					}
					i++;
				}
				//
				// Often there will be one pair which we didn't add
				// (in the case that there is on '&' at the end of
				// the query string).
				//
				if( !key.empty() ) args[key] = value;
			}
		}
	  public:
		urlImpl() {}

		//
		// bool fromString( util::string aUrl )
		//
		/// Try to extract a URL from the aUrl parameter. Return true
		/// if the extraction was successful, or false if it failed.
		///
		/// \param	aUrl	the URL to parse
		/// \return	true	the URL parsed correctly,
		///		false	the URL could not be parsed.
		//
		bool fromString( util::string aUrl ) {
			util::regexp* r = util::regexp::fromStringAndProcessor( URL_REGEXP, this );
			//
			// Make sure that the regexp was valid.
			//
			if( !r ) return false;
			//
			// Blank everything out before trying.
			//
			presentParts = 0;

			bool rval = r->match( aUrl );
			r->releaseStringProcessor( this );
			delete r;

			if( !rval ) {
				presentParts = 0;
				theUrl = "";
			}

			return rval;
		}

		virtual util::string getCompleteUrl() const {
			return getUrlWithoutParts( 0 );
		}

		virtual util::string getPrintableUrl() const {
			return getUrlWithoutParts( kPassword );
		}

		virtual util::string getUrlWithoutParts( unsigned int parts ) const {
			util::string aUrl = protocol + "://";
			if( presentParts & kUsername ) aUrl += username;
			if( (presentParts & kPassword) && (presentParts & kUsername) && ((parts & kPassword) == 0) ) {
				aUrl += ":"; aUrl += password;
			}
			if( (presentParts & (kUsername|kPassword)) != 0 )
				aUrl += "@";
			if( presentParts & kHostname ) aUrl += hostname;
			if( presentParts & kPort ) {
				char buffer[256];

				snprintf( buffer, 255, "%d", port );
				aUrl += ":";
				aUrl += buffer;
			}
			aUrl += "/";
			if( ((parts & kPath) == 0) && (presentParts & kPath) )
				aUrl += path;
			if( ((parts & kQuery) == 0) && (presentParts & kQuery) ) {
				aUrl += "?";
				aUrl += query;
			}

			return aUrl;
		}

		virtual util::string getProtocol() const {
			if( presentParts | kProtocol ) return protocol;
			return "";
		}

		virtual util::string getUsername() const {
			if( presentParts | kUsername ) return username;
			return "";
		}

		virtual util::string getPassword() const {
			if( presentParts | kPassword ) return password;
			return "";
		}

		virtual util::string getHostname() const {
			if( presentParts | kHostname ) return hostname;
			return "";
		}

		virtual int getPort() const {
			if( presentParts | kPort ) return port;
			return -1;
		}

		virtual util::string getPath() const {
			if( presentParts | kPath ) return path;
			return "";
		}

		virtual void setPath( util::string p ) {
			path = p;
			if( p.empty() ) presentParts &= ~kPath;
			else presentParts |= kPath;
		}

		virtual unsigned int getNumberArguments() const {
			return args.size();
		}

		virtual util::string getArgumentKey( unsigned int i ) const {
			if( i >= getNumberArguments() ) return "";
			argMap::const_iterator a = args.begin();
			while( i > 0 ) { i--; a++; if( a == args.end() ) return ""; }
			return a->first;
		}

		virtual util::string getArgumentValue( unsigned int i ) const {
			if( i >= getNumberArguments() ) return "";
			argMap::const_iterator a = args.begin();
			while( i > 0 ) { i--; a++; if( a == args.end() ) return ""; }
			return a->second;
		}

		virtual util::string getArgumentValue( util::string key ) const {
			argMap::const_iterator a = args.find( key );
			if( a != args.end() ) return a->second;
			return "";
		}

		virtual int getPresentParts() const {
			return presentParts;
		}

		virtual bool equals( const url* otherUrl ) const {
			//
			// This isn't strictly true, because some name could
			// resolve to an IP address, i.e.:
			//  http://localhost/file == http://127.0.0.1/file
			//
			return (getCompleteUrl() == otherUrl->getCompleteUrl());
		}
	};

	url::~url() {}

	url* url::fromString( util::string theUrl ) {
		urlImpl* ui = new urlImpl();
		if( ui->fromString( theUrl ) ) return ui;
		delete ui;
		return NULL;
	}

	url* url::fromUrl( const url* theUrl ) {
		if( !theUrl ) return NULL;
		urlImpl* ui = new urlImpl();
		if( ui->fromString( theUrl->getCompleteUrl() ) ) return ui;
		delete ui;
		return NULL;
	}
};

